<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Fabric.js 导出 PPT 示例</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.11.0/dist/pptxgen.bundle.min.js"></script>
  
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#165DFF',
            secondary: '#36D399',
            accent: '#FF6B6B',
            dark: '#1E293B',
            light: '#F8FAFC'
          },
          fontFamily: {
            inter: ['Inter', 'sans-serif'],
          },
        },
      }
    }
  </script>
  
  <style type="text/tailwindcss">
    @layer utilities {
      .content-auto {
        content-visibility: auto;
      }
      .canvas-container {
        @apply rounded-lg shadow-lg overflow-hidden border border-gray-200 bg-white;
      }
      .tool-btn {
        @apply w-10 h-10 flex items-center justify-center rounded-lg transition-all duration-200 hover:bg-primary/10 active:bg-primary/20;
      }
      .tool-btn.active {
        @apply bg-primary/10 text-primary;
      }
      .btn-primary {
        @apply bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg active:shadow-none active:translate-y-0.5;
      }
      .btn-secondary {
        @apply bg-gray-200 hover:bg-gray-300 text-dark font-medium py-2 px-4 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg active:shadow-none active:translate-y-0.5;
      }
    }
  </style>
</head>
<body class="bg-gray-50 font-inter text-dark min-h-screen flex flex-col">
  <!-- 顶部导航 -->
  <header class="bg-white shadow-sm sticky top-0 z-50">
    <div class="container mx-auto px-4 py-3 flex items-center justify-between">
      <div class="flex items-center space-x-2">
        <i class="fa-solid fa-paint-brush text-primary text-2xl"></i>
        <h1 class="text-xl font-bold">Canvas 导出 PPT</h1>
      </div>
      <div class="flex items-center space-x-3">
        <button id="save-ppt" class="btn-primary flex items-center space-x-2">
          <i class="fa-solid fa-file-powerpoint"></i>
          <span>导出可编辑PPT</span>
        </button>
        <button id="save-ppt-image" class="btn-secondary flex items-center space-x-2">
          <i class="fa-regular fa-image"></i>
          <span>导出图片PPT</span>
        </button>
      </div>
    </div>
  </header>

  <!-- 主内容区 -->
  <main class="flex-grow container mx-auto px-4 py-6 flex flex-col md:flex-row gap-6">
    <!-- 工具栏 -->
    <div class="w-full md:w-64 flex flex-col gap-4">
      <div class="bg-white rounded-xl shadow-md p-4">
        <h2 class="font-semibold text-lg mb-3">绘图工具</h2>
        <div class="grid grid-cols-4 gap-2">
          <button id="select-tool" class="tool-btn active" title="选择">
            <i class="fa-solid fa-mouse-pointer"></i>
          </button>
          <button id="rect-tool" class="tool-btn" title="矩形">
            <i class="fa-regular fa-square"></i>
          </button>
          <button id="circle-tool" class="tool-btn" title="圆形">
            <i class="fa-regular fa-circle"></i>
          </button>
          <button id="triangle-tool" class="tool-btn" title="三角形">
            <i class="fa-solid fa-triangle"></i>
          </button>
          <button id="text-tool" class="tool-btn" title="文本">
            <i class="fa-solid fa-font"></i>
          </button>
          <button id="image-tool" class="tool-btn" title="图片">
            <i class="fa-regular fa-image"></i>
          </button>
          <button id="line-tool" class="tool-btn" title="直线">
            <i class="fa-solid fa-minus"></i>
          </button>
          <button id="arrow-tool" class="tool-btn" title="箭头">
            <i class="fa-solid fa-arrow-right"></i>
          </button>
        </div>
      </div>
      
      <div class="bg-white rounded-xl shadow-md p-4">
        <h2 class="font-semibold text-lg mb-3">属性设置</h2>
        <div class="space-y-4">
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">填充颜色</label>
            <input type="color" id="fill-color" class="w-full h-10 rounded border border-gray-300" value="#ffffff">
          </div>
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">边框颜色</label>
            <input type="color" id="stroke-color" class="w-full h-10 rounded border border-gray-300" value="#000000">
          </div>
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">边框宽度</label>
            <input type="range" id="stroke-width" min="1" max="10" value="2" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
          </div>
          <div id="text-options" class="hidden">
            <label class="block text-sm font-medium text-gray-700 mb-1">字体大小</label>
            <input type="number" id="font-size" min="8" max="72" value="24" class="w-full h-10 rounded border border-gray-300">
          </div>
        </div>
      </div>
      
      <div class="bg-white rounded-xl shadow-md p-4">
        <h2 class="font-semibold text-lg mb-3">操作</h2>
        <div class="space-y-3">
          <button id="delete-selected" class="btn-secondary w-full flex items-center justify-center space-x-2">
            <i class="fa-solid fa-trash"></i>
            <span>删除选中</span>
          </button>
          <button id="clear-canvas" class="btn-secondary w-full flex items-center justify-center space-x-2">
            <i class="fa-solid fa-eraser"></i>
            <span>清空画布</span>
          </button>
        </div>
      </div>
    </div>
    
    <!-- 画布区域 -->
    <div class="flex-grow flex flex-col">
      <div class="bg-white rounded-xl shadow-md p-4 mb-4">
        <h2 class="font-semibold text-lg">画布</h2>
      </div>
      <div class="flex-grow relative">
        <div id="canvas-container" class="canvas-container w-full h-full mx-auto">
          <canvas id="fabric-canvas" class="w-full h-full"></canvas>
        </div>
      </div>
    </div>
  </main>

  <!-- 底部信息 -->
  <footer class="bg-white py-4 border-t border-gray-200">
    <div class="container mx-auto px-4 text-center text-gray-500 text-sm">
      <p>使用 Fabric.js 和 PptxGenJS 构建的画布导出 PPT 示例</p>
    </div>
  </footer>

  <script>
    // 初始化 Fabric.js 画布
    const canvas = new fabric.Canvas('fabric-canvas', {
      width: 800,
      height: 500,
      backgroundColor: '#f9f9f9'
    });
    
    // 调整画布大小以适应容器
    function resizeCanvas() {
      const container = document.getElementById('canvas-container');
      const containerWidth = container.offsetWidth;
      const containerHeight = container.offsetHeight;
      
      // 设置画布大小并保持比例
      const ratio = canvas.getWidth() / canvas.getHeight();
      let newWidth, newHeight;
      
      if (containerWidth / containerHeight > ratio) {
        newHeight = containerHeight;
        newWidth = newHeight * ratio;
      } else {
        newWidth = containerWidth;
        newHeight = newWidth / ratio;
      }
      
      canvas.setWidth(newWidth);
      canvas.setHeight(newHeight);
      canvas.renderAll();
    }
    
    // 监听窗口大小变化，调整画布
    window.addEventListener('resize', resizeCanvas);
    resizeCanvas();
    
    // 初始化绘图工具
    let currentTool = 'select';
    const toolButtons = {
      'select': document.getElementById('select-tool'),
      'rect': document.getElementById('rect-tool'),
      'circle': document.getElementById('circle-tool'),
      'triangle': document.getElementById('triangle-tool'),
      'text': document.getElementById('text-tool'),
      'image': document.getElementById('image-tool'),
      'line': document.getElementById('line-tool'),
      'arrow': document.getElementById('arrow-tool')
    };
    
    // 工具选择
    function setActiveTool(tool) {
      // 移除所有按钮的活动状态
      Object.values(toolButtons).forEach(btn => btn.classList.remove('active'));
      
      // 设置当前工具和按钮状态
      currentTool = tool;
      toolButtons[tool].classList.add('active');
      
      // 显示/隐藏文本选项
      document.getElementById('text-options').classList.toggle('hidden', tool !== 'text');
      
      // 更新画布交互模式
      canvas.isDrawingMode = false;
      canvas.selection = true;
      
      // 清除所有活动对象
      canvas.discardActiveObject();
      canvas.renderAll();
    }
    
    // 绑定工具选择事件
    Object.keys(toolButtons).forEach(tool => {
      toolButtons[tool].addEventListener('click', () => setActiveTool(tool));
    });
    
    // 形状创建函数
    function createRect() {
      const rect = new fabric.Rect({
        left: 100,
        top: 100,
        width: 100,
        height: 80,
        fill: document.getElementById('fill-color').value,
        stroke: document.getElementById('stroke-color').value,
        strokeWidth: parseInt(document.getElementById('stroke-width').value),
        originX: 'center',
        originY: 'center',
        hasControls: true,
        hasBorders: true
      });
      canvas.add(rect);
      canvas.setActiveObject(rect);
    }
    
    function createCircle() {
      const circle = new fabric.Circle({
        left: 100,
        top: 100,
        radius: 50,
        fill: document.getElementById('fill-color').value,
        stroke: document.getElementById('stroke-color').value,
        strokeWidth: parseInt(document.getElementById('stroke-width').value),
        originX: 'center',
        originY: 'center',
        hasControls: true,
        hasBorders: true
      });
      canvas.add(circle);
      canvas.setActiveObject(circle);
    }
    
    function createTriangle() {
      const triangle = new fabric.Triangle({
        left: 100,
        top: 100,
        width: 100,
        height: 80,
        fill: document.getElementById('fill-color').value,
        stroke: document.getElementById('stroke-color').value,
        strokeWidth: parseInt(document.getElementById('stroke-width').value),
        originX: 'center',
        originY: 'center',
        hasControls: true,
        hasBorders: true
      });
      canvas.add(triangle);
      canvas.setActiveObject(triangle);
    }
    
    function createText() {
      const text = new fabric.Textbox('输入文本', {
        left: 100,
        top: 100,
        width: 200,
        fontSize: parseInt(document.getElementById('font-size').value),
        fill: document.getElementById('stroke-color').value,
        originX: 'center',
        originY: 'center',
        hasControls: true,
        hasBorders: true,
        textAlign: 'center'
      });
      canvas.add(text);
      canvas.setActiveObject(text);
      text.enterEditing();
      text.selectAll();
    }
    
    function createLine(points, isArrow) {
      const line = new fabric.Line(points, {
        stroke: document.getElementById('stroke-color').value,
        strokeWidth: parseInt(document.getElementById('stroke-width').value),
        fill: document.getElementById('stroke-color').value,
        originX: 'center',
        originY: 'center',
        hasControls: true,
        hasBorders: true
      });
      
      if (isArrow) {
        line.set({
          strokeLineCap: 'round',
          strokeLineJoin: 'round',
          strokeWidth: parseInt(document.getElementById('stroke-width').value) + 1,
          arrowColor: document.getElementById('stroke-color').value
        });
      }
      
      canvas.add(line);
      canvas.setActiveObject(line);
    }
    
    // 绑定形状创建事件
    document.getElementById('rect-tool').addEventListener('click', createRect);
    document.getElementById('circle-tool').addEventListener('click', createCircle);
    document.getElementById('triangle-tool').addEventListener('click', createTriangle);
    document.getElementById('text-tool').addEventListener('click', createText);
    
    // 图片上传
    document.getElementById('image-tool').addEventListener('click', () => {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = 'image/*';
      
      input.onchange = (e) => {
        const file = e.target.files[0];
        if (!file) return;
        
        const reader = new FileReader();
        reader.onload = (event) => {
          fabric.Image.fromURL(event.target.result, (img) => {
            const imgInstance = img.set({
              left: 100,
              top: 100,
              originX: 'center',
              originY: 'center',
              hasControls: true,
              hasBorders: true
            }).scale(0.5);
            
            canvas.add(imgInstance);
            canvas.setActiveObject(imgInstance);
          });
        };
        reader.readAsDataURL(file);
      };
      
      input.click();
    });
    
    // 直线和箭头绘制
    let isDrawingLine = false;
    let startPoint = null;
    
    document.getElementById('line-tool').addEventListener('click', () => {
      setActiveTool('line');
      isDrawingLine = true;
    });
    
    document.getElementById('arrow-tool').addEventListener('click', () => {
      setActiveTool('arrow');
      isDrawingLine = true;
    });
    
    canvas.on('mouse:down', (e) => {
      if (isDrawingLine && e.pointer) {
        startPoint = { x: e.pointer.x, y: e.pointer.y };
      }
    });
    
    canvas.on('mouse:up', (e) => {
      if (isDrawingLine && startPoint && e.pointer) {
        const points = [startPoint.x, startPoint.y, e.pointer.x, e.pointer.y];
        createLine(points, currentTool === 'arrow');
        startPoint = null;
      }
    });
    
    // 监听属性变化
    document.getElementById('fill-color').addEventListener('input', (e) => {
      const activeObject = canvas.getActiveObject();
      if (activeObject) {
        if (activeObject.type !== 'line' && activeObject.type !== 'polyline' && activeObject.type !== 'polygon') {
          activeObject.set('fill', e.target.value);
          canvas.renderAll();
        }
      }
    });
    
    document.getElementById('stroke-color').addEventListener('input', (e) => {
      const activeObject = canvas.getActiveObject();
      if (activeObject) {
        activeObject.set('stroke', e.target.value);
        if (activeObject.type === 'textbox' || activeObject.type === 'text') {
          activeObject.set('fill', e.target.value);
        }
        canvas.renderAll();
      }
    });
    
    document.getElementById('stroke-width').addEventListener('input', (e) => {
      const activeObject = canvas.getActiveObject();
      if (activeObject) {
        activeObject.set('strokeWidth', parseInt(e.target.value));
        canvas.renderAll();
      }
    });
    
    document.getElementById('font-size').addEventListener('input', (e) => {
      const activeObject = canvas.getActiveObject();
      if (activeObject && (activeObject.type === 'textbox' || activeObject.type === 'text')) {
        activeObject.set('fontSize', parseInt(e.target.value));
        canvas.renderAll();
      }
    });
    
    // 删除选中对象
    document.getElementById('delete-selected').addEventListener('click', () => {
      const activeObject = canvas.getActiveObject();
      if (activeObject) {
        canvas.remove(activeObject);
      }
    });
    
    // 清空画布
    document.getElementById('clear-canvas').addEventListener('click', () => {
      if (confirm('确定要清空画布吗？')) {
        canvas.clear();
        canvas.setBackgroundColor('#f9f9f9', canvas.renderAll.bind(canvas));
      }
    });
    
    // 导出为可编辑的PPT
    document.getElementById('save-ppt').addEventListener('click', async () => {
      // 创建PPT实例
      const pptx = new PptxGenJS();
      
      // 添加幻灯片
      const slide = pptx.addSlide();
      
      // 设置幻灯片背景为白色
      slide.back = 'FFFFFF';
      
      // 获取Canvas上的所有元素
      const objects = canvas.getObjects();
      
      // 定义PPT尺寸（英寸）
      const pptWidth = 10;
      const pptHeight = 7.5;
      
      // 计算Canvas与PPT的宽高比
      const canvasRatio = canvas.getWidth() / canvas.getHeight();
      const pptRatio = pptWidth / pptHeight;
      
      // 计算缩放比例和偏移量，保持内容居中
      let scale, offsetX, offsetY;
      
      if (canvasRatio > pptRatio) {
        // Canvas更宽，以宽度为基准缩放
        scale = pptWidth / canvas.getWidth();
        offsetY = (pptHeight - canvas.getHeight() * scale) / 2;
        offsetX = 0;
      } else {
        // Canvas更高，以高度为基准缩放
        scale = pptHeight / canvas.getHeight();
        offsetX = (pptWidth - canvas.getWidth() * scale) / 2;
        offsetY = 0;
      }
      
      // 转换颜色格式: #RRGGBB -> RRGGBB
      function convertColor(color) {
        if (!color || color === 'transparent') return null;
        return color.replace('#', '');
      }
      
      // 处理每个对象的Promise数组
      const objectPromises = objects.map(obj => {
        return new Promise((resolve, reject) => {
          try {
            // 计算在PPT中的位置和大小
            let left = obj.left * scale + offsetX;
            let top = obj.top * scale + offsetY;
            let width, height;
            
            // 根据对象类型计算尺寸
            if (obj.type === 'circle') {
              // 圆形需要特别处理，使用直径作为宽度和高度
              width = obj.radius * 2 * (obj.scaleX || 1) * scale;
              height = obj.radius * 2 * (obj.scaleY || 1) * scale;
            } else {
              width = obj.width * (obj.scaleX || 1) * scale;
              height = obj.height * (obj.scaleY || 1) * scale;
            }
            
            // 考虑旋转角度
            if (obj.angle && obj.angle !== 0) {
              // 对于旋转的对象，计算包围盒的尺寸
              const radians = obj.angle * Math.PI / 180;
              const cos = Math.abs(Math.cos(radians));
              const sin = Math.abs(Math.sin(radians));
              const newWidth = obj.width * (obj.scaleX || 1) * cos + obj.height * (obj.scaleY || 1) * sin;
              const newHeight = obj.width * (obj.scaleX || 1) * sin + obj.height * (obj.scaleY || 1) * cos;
              
              width = newWidth * scale;
              height = newHeight * scale;
              
              // 调整位置以适应包围盒
              left = (obj.left - (newWidth - obj.width * (obj.scaleX || 1)) / 2) * scale + offsetX;
              top = (obj.top - (newHeight - obj.height * (obj.scaleY || 1)) / 2) * scale + offsetY;
            }
            
            // 根据对象类型添加到PPT
            switch (obj.type) {
              case 'rect':
                slide.addShape(pptx.shapes.RECTANGLE, {
                  x: left,
                  y: top,
                  w: width,
                  h: height,
                  fill: { color: convertColor(obj.fill) },
                  line: { color: convertColor(obj.stroke), width: obj.strokeWidth || 1 },
                  rotate: obj.angle || 0
                });
                resolve();
                break;
                
              case 'circle':
                slide.addShape(pptx.shapes.ELLIPSE, {
                  x: left,
                  y: top,
                  w: width,
                  h: height,
                  fill: { color: convertColor(obj.fill) },
                  line: { color: convertColor(obj.stroke), width: obj.strokeWidth || 1 },
                  rotate: obj.angle || 0
                });
                resolve();
                break;
                
              case 'triangle':
                // 确保三角形尺寸有效
                if (width <= 0 || height <= 0) {
                  console.warn('三角形尺寸无效，跳过导出');
                  resolve();
                  return;
                }
                
                slide.addShape(pptx.shapes.TRIANGLE, {
                  x: left,
                  y: top,
                  w: width,
                  h: height,
                  fill: { color: convertColor(obj.fill) },
                  line: { color: convertColor(obj.stroke), width: obj.strokeWidth || 1 },
                  rotate: obj.angle || 0
                });
                resolve();
                break;
                
              case 'textbox':
              case 'text':
                // 文本对象需要特殊处理
                slide.addText(obj.text, {
                  x: left,
                  y: top,
                  w: width,
                  h: height,
                  fontSize: obj.fontSize || 18,
                  color: convertColor(obj.fill) || '000000',
                  align: obj.textAlign || 'center',
                  fontFace: obj.fontFamily || 'Arial',
                  bold: obj.fontWeight === 'bold' || false,
                  italic: obj.fontStyle === 'italic' || false,
                  underline: obj.underline || false,
                  rotate: obj.angle || 0
                });
                resolve();
                break;
                
              case 'line':
                // 计算线条的起点和终点
                const x1 = obj.x1 * scale + offsetX;
                const y1 = obj.y1 * scale + offsetY;
                const x2 = obj.x2 * scale + offsetX;
                const y2 = obj.y2 * scale + offsetY;
                
                // 计算线条的宽度和高度
                const lineWidth = Math.abs(x2 - x1);
                const lineHeight = Math.abs(y2 - y1);
                
                // 检查是否是箭头
                const isArrow = obj.get('arrowColor') !== undefined;
                
                // 确定线条的左上角坐标
                const lineLeft = Math.min(x1, x2);
                const lineTop = Math.min(y1, y2);
                
                // 计算线条的角度
                const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
                
                // 创建线条
                slide.addShape(pptx.shapes.LINE, {
                  x: lineLeft,
                  y: lineTop,
                  w: lineWidth,
                  h: lineHeight,
                  line: { color: convertColor(obj.stroke), width: obj.strokeWidth || 1 },
                  endArrow: isArrow ? 'arrow' : 'none',
                  rotate: angle
                });
                resolve();
                break;
                
              case 'image':
                // 图片对象导出为base64图像
                obj.toDataURL({
                  format: 'png',
                  quality: 1.0
                }, (dataURL) => {
                  // 确保图片数据有效
                  if (!dataURL || !dataURL.startsWith('data:image')) {
                    console.error('无效的图片数据URL');
                    resolve();
                    return;
                  }
                  
                  slide.addImage({
                    data: dataURL,
                    x: left,
                    y: top,
                    w: width,
                    h: height,
                    rotate: obj.angle || 0
                  });
                  resolve();
                }, (error) => {
                  console.error('图片转换失败:', error);
                  resolve(); // 出错时继续处理其他元素
                });
                break;
                
              default:
                console.log('未知对象类型:', obj.type);
                resolve(); // 未知类型，直接resolve
            }
          } catch (error) {
            console.error('处理对象时出错:', obj.type, error);
            resolve(); // 出错时继续处理其他元素
          }
        });
      });
      
      // 等待所有对象处理完成
      try {
        await Promise.all(objectPromises);
        
        // 保存PPT文件
        pptx.writeFile({ fileName: 'Canvas导出_可编辑.pptx' })
          .then(() => {
            alert('可编辑PPT导出成功！');
          })
          .catch((error) => {
            console.error('导出PPT时出错:', error);
            alert('导出PPT失败，请重试。');
          });
      } catch (error) {
        console.error('处理PPT元素时出错:', error);
        alert('导出PPT失败，请重试。');
      }
    });
    
    // 导出为图片PPT (保留原功能)
    document.getElementById('save-ppt-image').addEventListener('click', () => {
      // 创建PPT实例
      const pptx = new PptxGenJS();
      
      // 添加幻灯片
      const slide = pptx.addSlide();
      
      // 设置幻灯片背景为白色
      slide.back = 'FFFFFF';
      
      // 从Canvas获取图像数据
      const dataURL = canvas.toDataURL({
        format: 'png',
        quality: 1.0,
        multiplier: 2 // 提高图像质量
      });
      
      // 计算图像在PPT中的位置和大小
      const pptWidth = 10; // PPT宽度，单位为英寸
      const pptHeight = 7.5; // PPT高度，单位为英寸
      
      const canvasRatio = canvas.getWidth() / canvas.getHeight();
      const pptRatio = pptWidth / pptHeight;
      
      let imgWidth, imgHeight;
      if (canvasRatio > pptRatio) {
        imgWidth = pptWidth;
        imgHeight = imgWidth / canvasRatio;
      } else {
        imgHeight = pptHeight;
        imgWidth = imgHeight * canvasRatio;
      }
      
      // 添加图像到幻灯片
      slide.addImage({
        data: dataURL,
        x: (pptWidth - imgWidth) / 2, // 水平居中
        y: (pptHeight - imgHeight) / 2, // 垂直居中
        w: imgWidth,
        h: imgHeight
      });
      
      // 保存PPT文件
      pptx.writeFile({ fileName: 'Canvas导出_图片.pptx' })
        .then(() => {
          alert('图片PPT导出成功！');
        })
        .catch((error) => {
          console.error('导出PPT时出错:', error);
          alert('导出PPT失败，请重试。');
        });
    });
    
    // 默认选择选择工具
    setActiveTool('select');
  </script>
</body>
</html>    